from math import e
import os
import shutil
import tempfile
import zipfile
import sys
from datetime import datetime
from file_utils import FileUtils, jar_path_to_system_path
from zip_rar_handler import ZipRarHandler
from excel_parser import ExcelParser
from jar_processor import JarProcessor
from utils import normalize_jar_path, jar_path_to_system_path, guess_class_package_path_from_bytes

## 更新执行核心类

class JarUpdaterCore:
    def __init__(self, gui):
        self.gui = gui
        self.clear_state()
        self.file_utils = FileUtils()
        self.zip_rar_handler = ZipRarHandler()
        self.excel_parser = ExcelParser()
        self.jar_processor = JarProcessor()
        self.jar_file_compression = {}  # 新增：存储主JAR文件压缩方式
        # 记录决策信息用于报告
        self.decisions = []

    # 修改_extract_main_jar方法，添加压缩方式记录
    def _extract_main_jar(self, jar_path, jar_temp_dir, log_callback):
        """解压主JAR包并记录时间戳和压缩方式"""
        try:
            with zipfile.ZipFile(jar_path, 'r') as jar_file:
                jar_file_list = [normalize_jar_path(name) for name in jar_file.namelist() if not name.endswith('/')]
                jar_dir_list = [normalize_jar_path(name.rstrip('/')) for name in jar_file.namelist() if name.endswith('/')]
            
            jar_file_timestamps = {}
            jar_dirs = set(jar_dir_list)
            self.jar_file_compression = {}  # 重置压缩方式字典
            
            with zipfile.ZipFile(jar_path, 'r') as jar_file:
                jar_file.extractall(jar_temp_dir)

                # 提取MANIFEST.MF（移到循环外部执行一次）
                self.jar_processor.extract_manifest(jar_path, jar_temp_dir, log_callback)
                
                for file_path in jar_file_list:
                    info = jar_file.getinfo(file_path)
                    dt = datetime(*info.date_time)
                    jar_file_timestamps[file_path] = dt.timestamp()
                    self.jar_file_compression[file_path] = info.compress_type  # 记录压缩方式
            
            log_callback(f"已解压主JAR包，包含 {len(jar_file_list)} 个文件和 {len(jar_dir_list)} 个目录，并记录了所有文件的压缩方式")
            return jar_file_timestamps, jar_dirs
        except Exception as e:
            raise Exception(f"解压主JAR包时出错: {str(e)}")

    # 修改_repackage_main_jar方法，使用记录的压缩方式
    def _repackage_main_jar(self, jar_temp_dir, output_path, jar_file_timestamps, jar_dirs, log_callback):
        """重新打包主JAR文件，使用原始压缩方式"""
        try:
            # 确保输出目录存在
            output_dir = os.path.dirname(output_path)
            if output_dir and not os.path.exists(output_dir):
                os.makedirs(output_dir, exist_ok=True)
            
            # 删除现有输出文件（如果存在）
            if os.path.exists(output_path):
                try:
                    os.remove(output_path)
                    log_callback(f"已删除现有文件: {output_path}")
                except Exception as e:
                    log_callback(f"警告: 无法删除现有文件 {output_path}, 将尝试覆盖: {str(e)}")
            
            # 创建新JAR文件，不指定默认压缩方式
            with zipfile.ZipFile(output_path, 'w') as new_jar:
                # 遍历解压的JAR内容并添加到新JAR
                for root, dirs, files in os.walk(jar_temp_dir):
                    # 计算相对路径
                    rel_path = os.path.relpath(root, jar_temp_dir)
                    normalized_rel_path = normalize_jar_path(rel_path)
                    
                    # 处理目录项
                    if normalized_rel_path != '.':  # 跳过根目录
                        dir_entry = f"{normalized_rel_path}/"
                        if dir_entry not in jar_dirs:
                            # 添加新目录
                            zinfo = zipfile.ZipInfo(dir_entry)
                            zinfo.external_attr = 0o40755 << 16  # 目录权限
                            zinfo.date_time = datetime.now().timetuple()[:6]
                            zinfo.compress_type = zipfile.ZIP_STORED
                            new_jar.writestr(zinfo, '')
                    
                    # 处理文件项
                    for file in files:
                        file_path = os.path.join(root, file)
                        rel_file_path = os.path.join(rel_path, file)
                        
                        # 修复根目录文件路径，移除多余的"./"前缀
                        if rel_path == '.':
                            normalized_file_path = normalize_jar_path(file)
                        else:
                            normalized_file_path = normalize_jar_path(rel_file_path)
                        
                        # 创建ZIP信息对象
                        zinfo = zipfile.ZipInfo(normalized_file_path)

                        # 设置时间戳（优先使用原始时间戳，新增文件使用当前时间）
                        if normalized_file_path in jar_file_timestamps:
                            timestamp = jar_file_timestamps[normalized_file_path]
                            zinfo.date_time = datetime.fromtimestamp(timestamp).timetuple()[:6]
                        else:
                            zinfo.date_time = datetime.now().timetuple()[:6]
                        
                        # 设置文件权限
                        if normalized_file_path.endswith('.class'):
                            zinfo.external_attr = 0o644 << 16  # 类文件权限
                        else:
                            zinfo.external_attr = 0o644 << 16  # 默认文件权限

                        # 使用记录的原始压缩方式，如果没有记录则根据文件类型设置
                        if normalized_file_path in self.jar_file_compression:
                            zinfo.compress_type = self.jar_file_compression[normalized_file_path]
                        elif normalized_file_path.endswith('.class'):
                            zinfo.compress_type = zipfile.ZIP_STORED
                        else:
                            zinfo.compress_type = zipfile.ZIP_DEFLATED
                        
                        # 添加文件到JAR
                        with open(file_path, 'rb') as f:
                            new_jar.writestr(zinfo, f.read())
            
            log_callback(f"主JAR文件已重新打包: {output_path}，保持原始文件的压缩方式")
            
        except Exception as e:
            raise Exception(f"重新打包主JAR时出错: {str(e)}")

    def clear_state(self):
        """清除核心状态数据"""
        self.update_details = []
        self.files_to_add = []
        self.files_to_replace = []
        self.files_to_delete = []
        self.zip_file_index = {}

    def find_file_in_zip(self, file_name):
        """在压缩包中查找指定文件名的文件"""
        if not self.zip_file_index:
            return None
        return self.zip_file_index.get(file_name, None)

    def _filter_zip_candidates_by_parent(self, filename_field, candidates):
        """当Excel的文件名包含 '父文件夹/文件' 形式时，按父级目录精确匹配候选项

        只保留父目录名与候选路径中紧邻文件名的目录相等的项。
        例如：filename_field = '中文文件夹/配置.properties'
        将仅匹配 path 末尾为 '.../中文文件夹/配置.properties' 的候选。
        """
        if not isinstance(filename_field, str) or '/' not in filename_field:
            return candidates
        folder_part, base_name = filename_field.rsplit('/', 1)
        folder_part = folder_part.strip('/').replace('\\', '/')
        base_name = base_name.strip()
        filtered = []
        for c in candidates or []:
            path = c.get('path', '')
            norm = path.replace('\\', '/')
            parts = [p for p in norm.split('/') if p]
            if len(parts) < 2:
                continue
            if parts[-1] != base_name:
                continue
            parent = parts[-2]
            if parent == folder_part:
                filtered.append(c)
        # 若严格匹配无结果，退回原始候选集合，避免丢失
        return filtered if filtered else candidates
    
    def _filter_zip_candidates_by_folder(self, folder_name, candidates, log_callback):
        """根据Excel中的"文件夹"列过滤补丁包中的候选文件
        
        优先匹配补丁包中指定文件夹下的文件。
        例如：folder_name = 'grp-plan'
        将优先匹配路径包含 'grp-plan/' 的候选文件
        
        Args:
            folder_name: Excel中指定的文件夹名称
            candidates: 候选文件列表
            log_callback: 日志回调函数
            
        Returns:
            过滤后的候选文件列表（如果无匹配则返回原候选列表）
        """
        if not folder_name or not candidates:
            return candidates
        
        # 标准化文件夹名称
        folder_name = folder_name.strip().replace('\\', '/')
        if not folder_name.endswith('/'):
            folder_name = folder_name + '/'
        
        filtered = []
        for c in candidates or []:
            path = c.get('path', '')
            norm = path.replace('\\', '/')
            
            # 匹配规则：路径中包含指定的文件夹名称
            # 支持 "folder/" 或 "/folder/" 的形式
            if f'/{folder_name}' in f'/{norm}' or norm.startswith(folder_name):
                filtered.append(c)
        
        if filtered:
            log_callback(f"根据文件夹'{folder_name.rstrip('/')}'过滤，从{len(candidates)}个候选文件缩小到{len(filtered)}个")
            return filtered
        else:
            log_callback(f"警告：未找到文件夹'{folder_name.rstrip('/')}'中的匹配文件，将使用所有候选")
            return candidates

    def perform_update(self, base_jar_path, update_zip_path, excel_path, output_path, replace_info_list, status_var, log_callback, progress_var, folder_replace_mode=False):
        """执行JAR包更新的实际操作"""
        temp_dir = None
        try:
            # 存储为实例变量
            self.folder_replace_mode = folder_replace_mode

            total_files = len(replace_info_list)
            status_var.set(f"准备更新，共需处理 {total_files} 个文件...")
            progress_var.set(5)
            log_callback(f"开始更新操作，共需处理 {total_files} 个文件")
            
            # 创建临时目录
            jar_dir = os.path.dirname(base_jar_path)
            temp_dir = os.path.join(jar_dir, "temp")
            os.makedirs(temp_dir, exist_ok=True)
            jar_temp_dir = os.path.join(temp_dir, "jar_contents")
            zip_temp_dir = os.path.join(temp_dir, "zip_contents")
            os.makedirs(jar_temp_dir, exist_ok=True)
            os.makedirs(zip_temp_dir, exist_ok=True)
            
            # 索引压缩包文件
            status_var.set("正在索引压缩包中的文件...")
            self.zip_file_index = self.zip_rar_handler.index_zip_files_by_name(update_zip_path)
            file_count = sum(len(v) for v in self.zip_file_index.values())
            log_callback(f"已索引压缩包中的文件，共发现 {file_count} 个文件")

            # 统计Excel中涉及的文件名（便于找出额外文件）
            excel_filenames = set()
            for i in replace_info_list:
                if i.get('is_dir'):
                    # 文件夹在展开后统计，此处仅跳过
                    continue
                excel_filenames.add(i['filename'] if isinstance(i['filename'], str) else str(i['filename']))
            
            # 分析JAR包内容
            status_var.set("正在分析JAR包内容...")
            jar_entries = self.jar_processor.get_jar_entries(base_jar_path, temp_dir)
            jar_entries_set = set(jar_entries)

            # 解压主JAR包
            status_var.set("正在解压主JAR包...")
            progress_var.set(10)
            jar_file_timestamps, jar_dirs = self._extract_main_jar(base_jar_path, jar_temp_dir, log_callback)
            progress_var.set(25)

            # 区分删除/新增/替换文件
            self._split_add_replace_files(replace_info_list, jar_entries_set, temp_dir, jar_temp_dir, log_callback)

            # 先处理删除文件
            if self.files_to_delete:
                status_var.set("正在删除文件...")
                deleted, failed_deletes = self._process_delete_files(jar_temp_dir, log_callback, progress_var, temp_dir)
                log_callback(f"删除文件完成：成功 {deleted} 个，失败 {failed_deletes} 个")
            
            #删除对应文件夹
            for replace_info in replace_info_list:
                # 获取要删除的文件夹路径
                if not folder_replace_mode or not replace_info["is_dir"]:
                    continue
                file_path = replace_info.get('dest')

                dir_path = os.path.dirname(file_path)
                if not dir_path.endswith('/'):
                    dir_path = dir_path+"/"

                full_dir_path = os.path.normpath(os.path.join(jar_temp_dir, dir_path))
                exists = os.path.exists(full_dir_path)
                if exists:
                    log_callback(f"删除文件夹: {full_dir_path}")
                    shutil.rmtree(full_dir_path, onerror=self.file_utils.handle_remove_readonly)
                    log_callback(f"已成功删除文件夹: {full_dir_path}")
            
            # 解压替换文件压缩包
            status_var.set("正在解压替换文件压缩包...")
            self.zip_rar_handler.extract_zip_contents(update_zip_path, zip_temp_dir)
            log_callback("替换文件压缩包已解压")
            progress_var.set(30)

            # 发现压缩包中Excel未列出的额外文件（按名称维度）
            zip_all_names = set(self.zip_file_index.keys())
            extra_names = sorted(list(zip_all_names - excel_filenames))
            if extra_names:
                include_all = self._confirm_include_extras(extra_names, log_callback)
                if include_all:
                    # 将这些名称（按最新时间戳）作为新增目标，默认放在BOOT-INF/classes/根，根据包名尝试定位
                    for name in extra_names:
                        # 取最新文件
                        candidates = sorted(self.zip_file_index.get(name, []), key=lambda x: x['timestamp'], reverse=True)
                        if not candidates:
                            continue
                        chosen = candidates[0]
                        # 推断包路径
                        src_path = os.path.join(zip_temp_dir, chosen['path'].replace('/', os.sep))
                        dest_rel = f"BOOT-INF/classes/{name}"
                        try:
                            if name.endswith('.class') and os.path.exists(src_path):
                                with open(src_path, 'rb') as f:
                                    data = f.read()
                                pkg_guess = guess_class_package_path_from_bytes(data, expected_class_name=name[:-6])
                                if pkg_guess:
                                    dest_rel = f"BOOT-INF/classes/{normalize_jar_path(pkg_guess)}"
                        except Exception:
                            pass
                        info = {
                            'filename': name,
                            'dest': dest_rel,
                            'lib包': '',
                            '路径': '',
                            'row_num': -1,
                            'is_dir': False
                        }
                        # 基于是否已存在决定新增或替换
                        if dest_rel in jar_entries_set:
                            self.files_to_replace.append(info)
                        else:
                            self.files_to_add.append(info)
                    self.decisions.append({
                        'type': 'extras',
                        'action': 'include_all',
                        'count': len(extra_names)
                    })
                else:
                    self.decisions.append({
                        'type': 'extras',
                        'action': 'skip',
                        'count': len(extra_names)
                    })
                    
            
            # 处理新增文件
            status_var.set("正在从压缩包新增文件...")
            added, failed_adds = self._process_add_files(zip_temp_dir, jar_temp_dir, jar_file_timestamps, progress_var, log_callback, temp_dir)
            progress_var.set(50)
            
            # 处理替换文件
            status_var.set("正在替换文件...")
            replaced, failed_replaces = self._process_replace_files(zip_temp_dir, jar_temp_dir, jar_file_timestamps, log_callback, progress_var, temp_dir)
            progress_var.set(80)
            
            # 生成更新报告
            self.generate_update_report(jar_temp_dir, base_jar_path, update_zip_path, excel_path, log_callback)
            
            # 重新打包lib包
            status_var.set("正在重新打包修改过的lib包...")
            for lib_name in self.jar_processor.extracted_libs.keys():
                self.jar_processor.repackage_lib(lib_name, temp_dir, jar_temp_dir, log_callback)
            
            # 重新打包主JAR
            status_var.set("正在重新打包主JAR文件...")
            self._repackage_main_jar(jar_temp_dir, output_path, jar_file_timestamps, jar_dirs, log_callback)
            progress_var.set(95)
            
            # 验证文件
            self._verify_files(jar_temp_dir, temp_dir, log_callback)
            
            # 清理临时文件
            if temp_dir and os.path.exists(temp_dir):
                shutil.rmtree(temp_dir, onerror=self.file_utils.handle_remove_readonly)
            
            # 完成更新
            self._finish_update(status_var, progress_var, log_callback, added, replaced, failed_adds, failed_replaces, output_path)
            
            # 检查成功率是否为100%
            total_operations = added + replaced + failed_adds + failed_replaces
            success_rate = (added + replaced) / total_operations * 100 if total_operations > 0 else 100
            
            # 如果成功率不是100%，返回False
            if success_rate < 100:
                return False
            
            # 更新成功且成功率为100%时返回True
            return True
        except Exception as e:
            self._handle_update_error(e, temp_dir, status_var, log_callback)
            # 添加返回值：更新失败返回False
            return False

    def _split_add_replace_files(self, replace_info_list, jar_entries_set, temp_dir, jar_temp_dir, log_callback):
        """区分需要新增和替换的文件"""
        self.files_to_add = []
        self.files_to_replace = []
        self.files_to_delete = []
        
        for info in replace_info_list:
            # 删除优先：若Excel标记删除，则加入删除列表并跳过后续新增/替换判断
            if info.get('删除文件'):
                self.files_to_delete.append(info)
                continue
            dest_path = info['dest']
            if info["is_dir"]:
                filename = info["filename"].replace("//","")
                dest_path = dest_path.replace("//","/")
                
                #根据文件夹名称(filename)获取压缩包（self.zip_file_index）对应文件夹下所有文件
                folder_files = self.file_utils.find_file_in_zip(filename, self.zip_file_index)
                if folder_files:
                    for folder_file in folder_files:
                        # 分割路径为列表
                        path_parts = dest_path.split('/')
                        # 移除倒数第二个元素，保留其他部分
                        new_parts = path_parts[:-2] + path_parts[-1:]
                        # 重新拼接路径
                        file_path = '/'.join(new_parts)

                        normalized_folder = filename if filename.endswith('/') else f"{filename}/"
                        zipFilePath = ''
                        if folder_file['path'].startswith(normalized_folder):
                            zipFilePath = folder_file['path'][folder_file['path'].index(normalized_folder):]
                        else:
                            zipFilePath = folder_file['path'][folder_file['path'].index('/'+normalized_folder):]

                        file_path = os.path.join(file_path, zipFilePath.lstrip('/'))
                        
                        file_exist = self.split_add_replace_file(file_path, temp_dir, jar_entries_set, jar_temp_dir, log_callback)

                        infoCopy = info.copy()
                        infoCopy["filename"] = file_path.split('/')[-1]
                        infoCopy["dest"] = file_path
                        infoCopy["is_dir"] = False

                        if file_exist:
                            self.files_to_replace.append(infoCopy)
                        else:
                            self.files_to_add.append(infoCopy) 

            else:
                exist = self.split_add_replace_file(dest_path, temp_dir, jar_entries_set, jar_temp_dir, log_callback)
                if exist:
                    self.files_to_replace.append(info)
                else:
                    self.files_to_add.append(info)  

    def _process_delete_files(self, jar_temp_dir, log_callback, progress_var, temp_dir):
        """处理删除文件（支持主JAR与lib内部文件）"""
        deleted = 0
        failed = 0
        total = len(self.files_to_delete)

        for i, info in enumerate(self.files_to_delete):
            try:
                dest_jar_path = info['dest']
                lib_package = info['lib包']
                current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

                if lib_package:
                    # 删除lib包内部文件
                    lib_temp_dir = self.jar_processor.extract_lib_package(lib_package, jar_temp_dir, temp_dir, log_callback)
                    lib_inner_path = dest_jar_path.replace(f"BOOT-INF/lib/{lib_package}/", "")
                    dest_system_path = jar_path_to_system_path(lib_inner_path, lib_temp_dir)
                    if os.path.exists(dest_system_path):
                        os.remove(dest_system_path)
                        # 同步时间戳字典
                        if lib_package in self.jar_processor.lib_file_timestamps and lib_inner_path in self.jar_processor.lib_file_timestamps[lib_package]:
                            del self.jar_processor.lib_file_timestamps[lib_package][lib_inner_path]
                        log_callback(f"已删除lib内部文件: {dest_jar_path}")
                    else:
                        raise Exception(f"未找到待删除文件: {dest_jar_path}")
                else:
                    # 删除主JAR内文件（含 BOOT-INF/classes/... 或 BOOT-INF/lib/xxx.jar）
                    dest_system_path = jar_path_to_system_path(dest_jar_path, jar_temp_dir)
                    if os.path.exists(dest_system_path):
                        if os.path.isdir(dest_system_path):
                            shutil.rmtree(dest_system_path, onerror=self.file_utils.handle_remove_readonly)
                        else:
                            os.remove(dest_system_path)
                        log_callback(f"已删除文件: {dest_jar_path}")
                    else:
                        raise Exception(f"未找到待删除文件: {dest_jar_path}")

                deleted += 1
                self.update_details.append({
                    '操作类型': '删除',
                    '文件名': info.get('filename', ''),
                    '目标路径': dest_jar_path,
                    '操作时间': current_time,
                    '状态': '成功'
                })
            except Exception as e:
                failed += 1
                self._log_update_failure(info, '删除', str(e), 'N/A')
                log_callback(f"Excel第{info['row_num']}行 - 删除文件失败: {str(e)}")

            if total > 0:
                progress = 25 + (5 * (i + 1) / total)
                progress_var.set(min(progress, 30))

        return deleted, failed

    def split_add_replace_file(self, dest_path, temp_dir, jar_entries_set, jar_temp_dir, log_callback):
        if dest_path.startswith("BOOT-INF/lib/"):
            # 分割路径获取lib名称和内部路径
            parts = dest_path.split("/")
            if len(parts) < 4:
                exists = False
            else:
                lib_name_with_ext = parts[2]
                lib_path_in_jar = f"BOOT-INF/lib/{lib_name_with_ext}"
                # 先检查lib包本身是否存在于主JAR中
                if lib_path_in_jar not in jar_entries_set:
                    return False
                inner_path = "/".join(parts[3:])
                log_callback(f"解析到lib包 {lib_name_with_ext} 的内部路径: {inner_path}")  # 新增日志
                # 检查lib包是否已解压
                # 确保lib包已解压
                if lib_name_with_ext not in self.jar_processor.extracted_libs:
                    # 临时解压lib包以检查内部文件
                    lib_temp_dir= self.jar_processor.extract_lib_package(lib_name_with_ext, jar_temp_dir, temp_dir, log_callback)
                    if not os.path.exists(lib_temp_dir):
                        log_callback(f"解压失败：{lib_temp_dir} 不存在")
                    else:
                        # 检查解压后的文件列表
                        extracted_files = os.listdir(lib_temp_dir)
                        log_callback(f"{lib_name_with_ext} 解压成功，包含 {len(extracted_files)} 个文件")

                    log_callback(f"解压lib包到: {lib_temp_dir}")  # 新增日志
                # 检查解压后的lib包内是否存在目标文件
                lib_temp_dir = self.jar_processor.extracted_libs.get(lib_name_with_ext)
                if not lib_temp_dir:
                    return False    
                # 拼接lib内部文件的系统路径
                system_path = os.path.join(lib_temp_dir, inner_path.replace('/', os.sep))
                # 增加日志验证路径
                log_callback(f"检查文件是否存在: {system_path}")
                return os.path.exists(system_path)
        else:
            exists = dest_path in jar_entries_set
        # 若目标不存在，尝试在JAR中查找同名文件的其它路径，供后续决策
        if not exists and not dest_path.startswith('BOOT-INF/lib/'):
            filename = os.path.basename(dest_path)
            jar_candidates = [p for p in jar_entries_set if p.endswith('/' + filename) or p == filename]
            if jar_candidates:
                # 临时保存到实例映射，后续处理时读取
                if not hasattr(self, 'jar_path_candidates'):
                    self.jar_path_candidates = {}
                self.jar_path_candidates[dest_path] = jar_candidates
        return exists


    def _process_add_files(self, zip_temp_dir, jar_temp_dir, jar_file_timestamps, progress_var, log_callback, temp_dir):
        """处理新增文件"""
        added_files = 0
        failed_adds = 0
        total_adds = len(self.files_to_add)
        
        for i, info in enumerate(self.files_to_add):
            try:
                filename = info['filename']
                dest_jar_path = info['dest']
                lib_package = info['lib包']
                folder_name = info.get('文件夹', '')  # 获取文件夹列
                current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                
                # 支持 `父文件夹/文件` 形式的精确匹配
                base_name = filename.rsplit('/', 1)[-1]
                matching_files = self.find_file_in_zip(base_name)
                matching_files = self._filter_zip_candidates_by_parent(filename, matching_files)
                
                # 新增：优先根据Excel中的"文件夹"列过滤
                if folder_name and len(matching_files) > 1:
                    matching_files = self._filter_zip_candidates_by_folder(folder_name, matching_files, log_callback)
                
                if not matching_files:
                    raise Exception(f"压缩包中未找到文件: {filename}")
                # 多候选：让用户选择（GUI），Web默认选择最新
                if len(matching_files) > 1:
                    idx = self._resolve_choose_zip(filename, dest_jar_path, matching_files, log_callback)
                    zip_file_info = sorted(matching_files, key=lambda x: x['timestamp'], reverse=True)[0] if idx is None else matching_files[idx]
                else:
                    zip_file_info = matching_files[0]
                zip_file_path = zip_file_info['path']
                zip_file_path = normalize_jar_path(zip_file_path)
                zip_timestamp = zip_file_info['timestamp']
                src_path = os.path.join(zip_temp_dir, zip_file_path.replace('/', os.sep))
                
                if not os.path.exists(src_path):
                    raise Exception(f"压缩包中未找到文件: {src_path}")
                
                # 处理目标路径
                if lib_package:
                    lib_temp_dir = self.jar_processor.extract_lib_package(lib_package, jar_temp_dir, temp_dir, log_callback)
                    lib_inner_path = dest_jar_path.replace(f"BOOT-INF/lib/{lib_package}/", "")
                    dest_system_path = jar_path_to_system_path(lib_inner_path, lib_temp_dir)
                    self.jar_processor.lib_file_timestamps[lib_package][lib_inner_path] = zip_timestamp
                    
                    # 新增：双重处理逻辑 - 检查主jar包中是否也存在相同路径的文件
                    # 构建主jar包中的可能路径：BOOT-INF/classes/ + 路径 + 文件名
                    main_jar_path = f"BOOT-INF/classes/{info['路径']}/{filename}".replace('//', '/')
                    main_jar_system_path = jar_path_to_system_path(main_jar_path, jar_temp_dir)
                    
                    if os.path.exists(main_jar_system_path):
                        # 主jar包中也存在该文件，同时更新
                        try:
                            self.file_utils.copy_file_with_perms(src_path, main_jar_system_path)
                            jar_file_timestamps[main_jar_path] = zip_timestamp
                            log_callback(f"双重处理：同时更新主jar包中的文件 {main_jar_path}")
                            self.update_details.append({
                                '操作类型': '替换（主jar）',
                                '文件名': filename,
                                '目标路径': main_jar_path,
                                '源文件路径': zip_file_path,
                                '操作时间': current_time,
                                '状态': '成功'
                            })
                        except Exception as e:
                            log_callback(f"警告：双重处理主jar包文件失败 {main_jar_path}: {str(e)}")
                else:
                    # 路径不一致时，尝试决策（Excel路径 vs JAR现有路径 vs 包路径推断）
                    adjusted_dest = dest_jar_path
                    candidates = getattr(self, 'jar_path_candidates', {}).get(dest_jar_path)
                    pkg_guess_path = None
                    if filename.endswith('.class') and os.path.exists(src_path):
                        try:
                            with open(src_path, 'rb') as f:
                                data = f.read()
                            pkg_guess = guess_class_package_path_from_bytes(data, expected_class_name=filename[:-6])
                            if pkg_guess:
                                pkg_guess_path = f"BOOT-INF/classes/{normalize_jar_path(pkg_guess)}"
                        except Exception:
                            pass
                    if candidates:
                        idx, option_strings = self._resolve_path_mismatch(filename, dest_jar_path, candidates, pkg_guess_path, log_callback)
                        if idx is not None and 0 <= idx < len(option_strings):
                            choice = option_strings[idx]
                            if choice.startswith('Excel'):
                                adjusted_dest = dest_jar_path
                            elif choice.startswith('JAR现有路径: '):
                                adjusted_dest = choice.replace('JAR现有路径: ', '')
                            elif choice.startswith('按类包路径推断: '):
                                adjusted_dest = choice.replace('按类包路径推断: ', '')
                    dest_jar_path = adjusted_dest
                    dest_system_path = jar_path_to_system_path(dest_jar_path, jar_temp_dir)
                    jar_file_timestamps[dest_jar_path] = zip_timestamp
                os.makedirs(os.path.dirname(dest_system_path), exist_ok=True)
                self.file_utils.copy_file_with_perms(src_path, dest_system_path)
                
                added_files += 1
                log_callback(f"已从压缩包新增文件: {dest_jar_path} (源文件: {zip_file_path})")
                self.update_details.append({
                    '操作类型': '新增',
                    '文件名': filename,
                    '目标路径': dest_jar_path,
                    '源文件路径': zip_file_path,
                    '操作时间': current_time,
                    '状态': '成功'
                })
                
            except Exception as e:
                failed_adds += 1
                self._log_update_failure(info, '新增', str(e), zip_file_path if 'zip_file_path' in locals() else '未知')
                log_callback(f"Excel第{info['row_num']}行 - 新增文件失败: {str(e)}")
            
            # 更新进度
            if total_adds > 0:
                progress = 30 + (20 * (i + 1) / total_adds)
                progress_var.set(min(progress, 50))
        
        return added_files, failed_adds

    def _log_update_failure(self, info, operation_type, error_msg, source_path):
        """记录更新失败信息"""
        current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.update_details.append({
            '操作类型': operation_type,
            '文件名': info.get('filename', '未知'),
            '目标路径': info.get('dest', '未知'),
            '源文件路径': source_path,
            '操作时间': current_time,
            '状态': f'失败: {error_msg}'
        })

    def _process_replace_files(self, zip_temp_dir, jar_temp_dir, jar_file_timestamps, log_callback, progress_var, temp_dir):
        """处理需要替换的文件"""
        replaced_files = 0
        failed_replaces = 0
        total_replaces = len(self.files_to_replace)
        
        for i, info in enumerate(self.files_to_replace):
            try:
                filename = info['filename']
                dest_jar_path = info['dest']
                lib_package = info['lib包']
                folder_name = info.get('文件夹', '')  # 获取文件夹列
                current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                
                # 在压缩包中查找匹配文件
                # 支持 `父文件夹/文件` 形式的精确匹配
                base_name = filename.rsplit('/', 1)[-1]
                matching_files = self.find_file_in_zip(base_name)
                matching_files = self._filter_zip_candidates_by_parent(filename, matching_files)
                
                # 新增：优先根据Excel中的"文件夹"列过滤
                if folder_name and len(matching_files) > 1:
                    matching_files = self._filter_zip_candidates_by_folder(folder_name, matching_files, log_callback)
                
                if not matching_files:
                    raise Exception(f"压缩包中未找到文件: {filename}")
                
                # 选择文件：GUI询问，Web默认最新
                if len(matching_files) > 1:
                    idx = self._resolve_choose_zip(filename, dest_jar_path, matching_files, log_callback)
                    zip_file_info = sorted(matching_files, key=lambda x: x['timestamp'], reverse=True)[0] if idx is None else matching_files[idx]
                else:
                    zip_file_info = matching_files[0]
                zip_file_path = zip_file_info['path']
                zip_timestamp = zip_file_info['timestamp']

                src_path = os.path.join(zip_temp_dir, zip_file_path.replace('/', os.sep))
                
                if not os.path.exists(src_path):
                    raise Exception(f"压缩包解压后未找到文件: {src_path}")
                
                # 处理目标路径
                if lib_package:
                    # 若压缩包中包含同名lib整包，则优先整包替换
                    lib_whole_candidates = [c for c in matching_files if os.path.basename(c['path']) == lib_package]
                    if lib_whole_candidates:
                        do_replace = self._resolve_replace_whole_lib(lib_package, log_callback)
                        if do_replace:
                            # 直接替换主JAR中的该lib
                            lib_jar_path = os.path.join(jar_temp_dir, 'BOOT-INF', 'lib', lib_package)
                            os.makedirs(os.path.dirname(lib_jar_path), exist_ok=True)
                            whole_src = os.path.join(zip_temp_dir, lib_whole_candidates[0]['path'].replace('/', os.sep))
                            shutil.copy2(whole_src, lib_jar_path)
                            self.decisions.append({'type': 'lib_replace', 'lib': lib_package, 'mode': 'whole'})
                            log_callback(f"已整包替换lib: {lib_package}")
                            replaced_files += 1
                            continue
                    # 处理lib包内的文件替换
                    lib_temp_dir = self.jar_processor.extract_lib_package(lib_package, jar_temp_dir, temp_dir, log_callback)
                    lib_inner_path = dest_jar_path.replace(f"BOOT-INF/lib/{lib_package}/", "")
                    dest_system_path = jar_path_to_system_path(lib_inner_path, lib_temp_dir)

                    if zip_timestamp < self.jar_processor.lib_file_timestamps[lib_package][lib_inner_path]:
                        raise Exception(f"压缩包中的文件 {filename} 修改时间小于原始jar包里的修改时间，请检查文件是否被修改过")
                    # 更新lib包内文件的时间戳记录
                    if lib_package in self.jar_processor.lib_file_timestamps:
                        self.jar_processor.lib_file_timestamps[lib_package][lib_inner_path] = zip_timestamp
                    
                    # 新增：双重替换逻辑 - 检查主jar包中是否也存在相同路径的文件
                    # 构建主jar包中的可能路径：BOOT-INF/classes/ + 路径 + 文件名
                    main_jar_path = f"BOOT-INF/classes/{info['路径']}/{filename}".replace('//', '/')
                    main_jar_system_path = jar_path_to_system_path(main_jar_path, jar_temp_dir)
                    
                    if os.path.exists(main_jar_system_path):
                        # 主jar包中也存在该文件，进行双重替换
                        try:
                            self.file_utils.copy_file_with_perms(src_path, main_jar_system_path)
                            jar_file_timestamps[main_jar_path] = zip_timestamp
                            log_callback(f"双重替换：同时更新主jar包中的文件 {main_jar_path}")
                            self.update_details.append({
                                '操作类型': '替换（主jar）',
                                '文件名': filename,
                                '目标路径': main_jar_path,
                                '源文件路径': zip_file_path,
                                '操作时间': current_time,
                                '状态': '成功'
                            })
                        except Exception as e:
                            log_callback(f"警告：双重替换主jar包文件失败 {main_jar_path}: {str(e)}")
                else:
                    # 处理主JAR内的文件替换，必要时决策路径
                    adjusted_dest = dest_jar_path
                    candidates = getattr(self, 'jar_path_candidates', {}).get(dest_jar_path)
                    pkg_guess_path = None
                    if filename.endswith('.class') and os.path.exists(src_path):
                        try:
                            with open(src_path, 'rb') as f:
                                data = f.read()
                            pkg_guess = guess_class_package_path_from_bytes(data, expected_class_name=filename[:-6])
                            if pkg_guess:
                                pkg_guess_path = f"BOOT-INF/classes/{normalize_jar_path(pkg_guess)}"
                        except Exception:
                            pass
                    if candidates:
                        idx, option_strings = self._resolve_path_mismatch(filename, dest_jar_path, candidates, pkg_guess_path, log_callback)
                        if idx is not None and 0 <= idx < len(option_strings):
                            choice = option_strings[idx]
                            if choice.startswith('Excel'):
                                adjusted_dest = dest_jar_path
                            elif choice.startswith('JAR现有路径: '):
                                adjusted_dest = choice.replace('JAR现有路径: ', '')
                            elif choice.startswith('按类包路径推断: '):
                                adjusted_dest = choice.replace('按类包路径推断: ', '')
                    dest_jar_path = adjusted_dest
                    dest_system_path = jar_path_to_system_path(dest_jar_path, jar_temp_dir)

                    if zip_timestamp < jar_file_timestamps.get(dest_jar_path, 0):
                        raise Exception(f"压缩包中的文件 {filename} 修改时间小于原始jar包里的修改时间，请检查文件是否被修改过")

                    jar_file_timestamps[dest_jar_path] = zip_timestamp
                
                # 检查目标文件是否存在
                if not os.path.exists(dest_system_path):
                    raise Exception(f"目标文件不存在: {dest_jar_path}")
                
                # 复制文件并保留权限
                self.file_utils.copy_file_with_perms(src_path, dest_system_path)
                
                replaced_files += 1
                log_callback(f"已替换文件: {dest_jar_path} (源文件: {zip_file_path})")
                self.update_details.append({
                    '操作类型': '替换',
                    '文件名': filename,
                    '目标路径': dest_jar_path,
                    '源文件路径': zip_file_path,
                    '操作时间': current_time,
                    '状态': '成功'
                })
                
            except Exception as e:
                failed_replaces += 1
                zip_file_path = zip_file_path if 'zip_file_path' in locals() else '未知'
                self._log_update_failure(info, '替换', str(e), zip_file_path)
                log_callback(f"Excel第{info['row_num']}行 - 替换文件失败: {str(e)}")
            
            # 更新进度
            if total_replaces > 0:
                progress = 50 + (30 * (i + 1) / total_replaces)
                progress_var.set(min(progress, 80))
        
        return replaced_files, failed_replaces

    # 选择多候选源文件
    def _resolve_choose_zip(self, filename, dest_path, candidates, log_callback):
        if self.gui and hasattr(self.gui, 'choose_zip_mapping'):
            idx = self.gui.choose_zip_mapping(filename, dest_path, candidates)
            if idx is not None:
                self.decisions.append({'type': 'zip_choice', 'filename': filename, 'dest': dest_path, 'choice_idx': idx})
            else:
                self.decisions.append({'type': 'zip_choice', 'filename': filename, 'dest': dest_path, 'choice': 'auto_latest'})
            return idx
        # Web/无界面：默认选最新
        self.decisions.append({'type': 'zip_choice', 'filename': filename, 'dest': dest_path, 'choice': 'auto_latest'})
        return None

    # 选择路径不一致时的目标路径
    def _resolve_path_mismatch(self, filename, excel_dest, jar_candidates, pkg_guess_path, log_callback):
        if self.gui and hasattr(self.gui, 'choose_dest_path'):
            idx, options = self.gui.choose_dest_path(filename, excel_dest, jar_candidates, pkg_guess=pkg_guess_path)
            self.decisions.append({'type': 'path_choice', 'filename': filename, 'excel': excel_dest, 'options': options, 'idx': idx})
            return idx, options
        # Web：默认优先JAR现有路径，否则保持Excel
        opt = [excel_dest] + jar_candidates
        pick = 1 if len(jar_candidates) > 0 else 0
        self.decisions.append({'type': 'path_choice', 'filename': filename, 'excel': excel_dest, 'options': opt, 'idx': pick})
        return pick, [f'Excel指定路径: {excel_dest}'] + [f'JAR现有路径: {p}' for p in jar_candidates]

    # 是否整包替换lib
    def _resolve_replace_whole_lib(self, lib_name, log_callback):
        if self.gui and hasattr(self.gui, 'root'):
            from tkinter import messagebox
            return messagebox.askyesno('整包替换lib', f'检测到压缩包包含同名lib: {lib_name}\n是否整包替换该lib？')
        # Web默认整包替换
        return True

    # 处理额外文件
    def _confirm_include_extras(self, extra_names, log_callback):
        if self.gui and hasattr(self.gui, 'confirm_include_extras'):
            res = self.gui.confirm_include_extras(extra_names)
            return res
        # Web默认不替换
        log_callback(f"发现额外文件 {len(extra_names)} 个（Web模式默认跳过）")
        return False

    def _verify_files(self, jar_temp_dir, temp_dir, log_callback):
        """验证更新后的文件是否正确处理"""
        try:
            log_callback("开始验证更新结果...")
            verification_results = []
            
            # 验证新增文件
            for info in self.files_to_add:
                dest_jar_path = info['dest']
                lib_package = info['lib包']
                
                if lib_package:
                    lib_temp_dir = self.jar_processor.extracted_libs.get(lib_package)
                    if not lib_temp_dir:
                        verification_results.append(f"警告: 未找到lib包 {lib_package} 的解压目录，无法验证 {dest_jar_path}")
                        continue
                    
                    lib_inner_path = dest_jar_path.replace(f"BOOT-INF/lib/{lib_package}/", "")
                    dest_system_path = jar_path_to_system_path(lib_inner_path, lib_temp_dir)
                else:
                    dest_system_path = jar_path_to_system_path(dest_jar_path, jar_temp_dir)
                
                if os.path.exists(dest_system_path):
                    verification_results.append(f"验证成功: 新增文件 {dest_jar_path} 存在")
                else:
                    verification_results.append(f"验证失败: 新增文件 {dest_jar_path} 不存在")
            
            # 验证替换文件
            for info in self.files_to_replace:
                dest_jar_path = info['dest']
                lib_package = info['lib包']
                
                if lib_package:
                    lib_temp_dir = self.jar_processor.extracted_libs.get(lib_package)
                    if not lib_temp_dir:
                        verification_results.append(f"警告: 未找到lib包 {lib_package} 的解压目录，无法验证 {dest_jar_path}")
                        continue
                    
                    lib_inner_path = dest_jar_path.replace(f"BOOT-INF/lib/{lib_package}/", "")
                    dest_system_path = jar_path_to_system_path(lib_inner_path, lib_temp_dir)
                else:
                    dest_system_path = jar_path_to_system_path(dest_jar_path, jar_temp_dir)
                
                if os.path.exists(dest_system_path):
                    verification_results.append(f"验证成功: 替换文件 {dest_jar_path} 存在")
                else:
                    verification_results.append(f"验证失败: 替换文件 {dest_jar_path} 不存在")
            
            # 输出验证结果
            for result in verification_results:
                log_callback(result)
            
            # 检查是否有验证失败项
            failed_verifications = [res for res in verification_results if res.startswith("验证失败")]
            if failed_verifications:
                log_callback(f"警告: 发现 {len(failed_verifications)} 项验证失败，请检查")
                
        except Exception as e:
            log_callback(f"验证文件时出错: {str(e)}")

        # 验证主JAR中lib包是否已更新
        for lib_name in self.jar_processor.extracted_libs.keys():
            lib_jar_path = os.path.join(jar_temp_dir, "BOOT-INF", "lib", lib_name)
            if not os.path.exists(lib_jar_path):
                verification_results.append(f"验证失败: 主JAR中未找到更新后的 {lib_name}")
            else:
                # 可选：检查lib包内是否包含目标文件
                with zipfile.ZipFile(lib_jar_path, 'r') as lib_jar:
                    for info in self.files_to_replace + self.files_to_add:
                        if info['lib包'] == lib_name:
                            inner_path = info['dest'].replace(f"BOOT-INF/lib/{lib_name}/", "")
                            inner_path = normalize_jar_path(inner_path)
                            if inner_path in lib_jar.namelist():
                                verification_results.append(f"验证成功: {lib_name} 包含 {inner_path}")
                            else:
                                verification_results.append(f"验证失败: {lib_name} 缺失 {inner_path}")    

    def generate_update_report(self, jar_temp_dir, jar_path, zip_path, excel_file_path, log_callback):
        """生成详细的更新报告，记录所有文件操作的结果"""
        try:
            report_path = os.path.join(jar_temp_dir, "UPDATE_REPORT.txt")
            timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            
            with open(report_path, 'w', encoding='utf-8') as f:
                try:
                    # 报告头部信息
                    f.write("=" * 80 + "\n")
                    f.write(f"JAR包更新报告\n")
                    f.write(f"生成时间: {timestamp}\n")
                    f.write("=" * 80 + "\n\n")
                    
                    # 基础信息
                    f.write("【更新源信息】\n")
                    f.write(f"  原始JAR包路径: {os.path.abspath(jar_path)}\n")
                    f.write(f"  替换文件压缩包: {os.path.abspath(zip_path)}\n")
                    f.write(f"  配置Excel文件: {os.path.abspath(excel_file_path) if excel_file_path else '未使用'}\n")
                    f.write(f"  操作环境: Python {sys.version.split()[0]} on {sys.platform}\n")
                    f.write("\n" + "-" * 80 + "\n\n")
                    
                    # 统计信息
                    if not self.update_details:
                        f.write("【更新结果】\n")
                        f.write("  本次更新未执行任何文件操作\n")
                        log_callback("更新报告生成完成：未执行任何操作")
                        return
                    
                    # 计算统计数据
                    total = len(self.update_details)
                    success = sum(1 for item in self.update_details if item['状态'].startswith('成功'))
                    failed = total - success
                    added = sum(1 for item in self.update_details if item['操作类型'] == '新增')
                    replaced = sum(1 for item in self.update_details if item['操作类型'] == '替换')
                    
                    f.write("【更新统计】\n")
                    f.write(f"  总处理文件数: {total} 个\n")
                    f.write(f"  新增文件: {added} 个\n")
                    f.write(f"  替换文件: {replaced} 个\n")
                    f.write(f"  成功: {success} 个 ({success/total*100:.1f}%)\n")
                    f.write(f"  失败: {failed} 个 ({failed/total*100:.1f}%)\n")
                    f.write("\n" + "-" * 80 + "\n\n")
                    
                    # 决策记录
                    if self.decisions:
                        f.write("【交互/自动决策记录】\n")
                        for d in self.decisions:
                            f.write(f"  - {d}\n")
                        f.write("\n" + "-" * 80 + "\n\n")

                    # 详细操作记录
                    f.write("【详细操作记录】\n")
                    f.write(f"  格式说明：[序号] 操作类型 | 文件名 | 状态 | 路径\n")
                    f.write("  " + "-" * 74 + "\n")
                    
                    for i, item in enumerate(self.update_details, 1):
                        f.write(f"[{i}] 操作类型: {item['操作类型']}\n")
                        f.write(f"   文件名: {item['文件名']}\n")
                        f.write(f"   目标路径: {item['目标路径']}\n")
                        f.write(f"   源文件路径: {item.get('源文件路径', 'N/A')}\n")
                        if '旧文件路径' in item:
                            f.write(f"   旧文件路径: {item['旧文件路径']}\n")
                        f.write(f"   操作时间: {item['操作时间']}\n")
                        f.write(f"   状态: {item['状态']}\n")
                        f.write("-" * 80 + "\n")
                    
                    f.write("\n" + "=" * 80 + "\n")
                    f.write("报告结束 - 此报告已包含在更新后的JAR包根目录\n")
                    
                    log_callback(f"更新报告已生成：{report_path}")
                    
                except Exception as e:
                    error_msg = f"生成更新报告时出错：{str(e)}"
                    log_callback(error_msg)
                    # 即使报告生成失败，也继续执行后续流程
                    if 'f' in locals():
                        f.write(f"\n警告：报告生成过程中发生错误 - {error_msg}\n")
            
            # 新增：将报告复制到主JAR包同目录
            try:
                # 获取主JAR包所在目录
                jar_dir = os.path.dirname(os.path.abspath(jar_path))
                # 目标报告路径
                target_report_path = os.path.join(jar_dir, "UPDATE_REPORT.txt")
                # 复制报告文件
                self.file_utils.copy_file_with_perms(report_path, target_report_path)
                log_callback(f"更新报告已复制到主JAR包同目录：{target_report_path}")
            except Exception as e:
                log_callback(f"复制报告到主JAR包同目录时出错：{str(e)}")
                # 即使复制失败，也不影响更新流程
            
        except Exception as e:
            log_callback(f"无法创建报告文件：{str(e)}")                

    def _log_update_failure(self, info, operation_type, error_msg, source_path):
        """记录更新失败的详细信息"""
        self.update_details.append({
            '操作类型': operation_type,
            '文件名': info['filename'],
            '目标路径': info['dest'],
            '源文件路径': source_path,
            '操作时间': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            '状态': f'失败: {error_msg}',
            'Excel行号': info['row_num']
        })        

    def _finish_update(self, status_var, progress_var, log_callback, added, replaced, failed_adds, failed_replaces, output_path):
        """完成更新流程并输出总结信息"""
        total_operations = added + replaced + failed_adds + failed_replaces
        success_rate = (added + replaced) / total_operations * 100 if total_operations > 0 else 100
                
        status_var.set(f"更新完成 - 成功: {added + replaced}, 失败: {failed_adds + failed_replaces}")
        progress_var.set(100)
                
        log_callback("\n" + "=" * 60)
        log_callback(f"更新操作总结:")
        log_callback(f"  总处理文件数: {total_operations}")
        log_callback(f"  新增文件: {added} 个 (失败: {failed_adds} 个)")
        log_callback(f"  替换文件: {replaced} 个 (失败: {failed_replaces} 个)")
        log_callback(f"  成功率: {success_rate:.1f}%")
        log_callback(f"  最终JAR文件路径: {output_path}")
        log_callback("=" * 60 + "\n")
        log_callback("更新流程已完成！")

    def _handle_update_error(self, exception, temp_dir, status_var, log_callback):
        """处理更新过程中发生的错误"""
        error_msg = f"更新过程中发生错误: {str(exception)}"
        log_callback(f"ERROR: {error_msg}")
        status_var.set("更新失败")
                
        # 清理临时文件（如果存在）
        if temp_dir and os.path.exists(temp_dir):
            try:
                shutil.rmtree(temp_dir, onerror=self.file_utils.handle_remove_readonly)
                log_callback("已清理临时文件")
            except Exception as e:
                log_callback(f"清理临时文件时出错: {str(e)}")
                
        # 抛出异常供上层处理
        raise Exception(error_msg)